import { InviteUsersResponse, OIDCUser, User } from "@budibase/types"

import { accounts as _accounts, events, tenancy } from "@budibase/backend-core"
import { mocks as featureMocks } from "@budibase/backend-core/tests"
import * as userSdk from "../../../../sdk/users"
import { TestConfiguration, mocks, structures } from "../../../../tests"

jest.mock("nodemailer")
const sendMailMock = mocks.email.mock()

const accounts = jest.mocked(_accounts)

describe("/api/global/users", () => {
  const config = new TestConfiguration()

  beforeAll(async () => {
    await config.beforeAll()
  })

  afterAll(async () => {
    await config.afterAll()
  })

  beforeEach(async () => {
    jest.clearAllMocks()
    featureMocks.licenses.useCloudFree()
    await mocks.licenses.setUsersQuota(1000)
    await mocks.licenses.setCreatorsQuota(1000)
  })

  async function createBuilderUser() {
    const saveResponse = await config.api.users.saveUser(
      structures.users.builderUser(),
      200
    )
    const { body: user } = await config.api.users.getUser(saveResponse.body._id)
    await config.login(user)
    return user
  }

  describe("POST /api/global/users/invite", () => {
    it("should be able to generate an invitation", async () => {
      const email = structures.users.newEmail()
      const { code, res } = await config.api.users.sendUserInvite(
        sendMailMock,
        email
      )

      expect(res.body?.message).toBe("Invitation has been sent.")
      expect(res.body?.unsuccessful.length).toBe(0)
      expect(res.body?.successful.length).toBe(1)
      expect(res.body?.successful[0].email).toBe(email)

      expect(sendMailMock).toHaveBeenCalled()
      expect(code).toBeDefined()
      expect(events.user.invited).toHaveBeenCalledTimes(1)
    })

    it("should not be able to generate an invitation for existing user", async () => {
      const { code, res } = await config.api.users.sendUserInvite(
        sendMailMock,
        config.user!.email,
        400
      )

      expect(res.body.message).toBe(`Unavailable`)
      expect(sendMailMock).toHaveBeenCalledTimes(0)
      expect(code).toBeUndefined()
      expect(events.user.invited).toHaveBeenCalledTimes(0)
    })

    it("should not invite the same user twice", async () => {
      const email = structures.users.newEmail()
      await config.api.users.sendUserInvite(sendMailMock, email)

      jest.clearAllMocks()

      const { code, res } = await config.api.users.sendUserInvite(
        sendMailMock,
        email,
        400
      )

      expect(res.body.message).toBe(`Unavailable`)
      expect(sendMailMock).toHaveBeenCalledTimes(0)
      expect(code).toBeUndefined()
      expect(events.user.invited).toHaveBeenCalledTimes(0)
    })

    it("should not allow creator users to access single invite endpoint", async () => {
      const user = await createBuilderUser()

      const { res } = await config.withUser(user, () =>
        config.api.users.sendUserInvite(
          sendMailMock,
          structures.users.newEmail(),
          403
        )
      )
      expect(res.body.message).toBe("Admin user only endpoint.")
    })

    it("should be able to create new user from invite", async () => {
      const email = structures.users.newEmail()
      const { code } = await config.api.users.sendUserInvite(
        sendMailMock,
        email
      )

      const res = await config.api.users.acceptInvite(code)

      expect(res.body._id).toBeDefined()
      const user = await config.getUser(email)
      expect(user).toBeDefined()
      expect(user!._id).toEqual(res.body._id)
      expect(events.user.inviteAccepted).toHaveBeenCalledTimes(1)
      expect(events.user.inviteAccepted).toHaveBeenCalledWith(user)
    })
  })

  describe("POST /api/global/users/invite/:code/:role", () => {
    it("should be able to add workspace id to invite", async () => {
      const email = structures.users.newEmail()
      const { code } = await config.api.users.sendUserInvite(
        sendMailMock,
        email
      )
      const appId = "app_123456789"
      const role = "BASIC"

      const res = await config.withApp(appId, () =>
        config.api.users.addWorkspaceIdToInvite(code, role)
      )

      expect(res.body.info.apps).toBeDefined()
      expect(res.body.info.apps[appId]).toBe(role)
    })

    it("should handle invalid invite code", async () => {
      const appId = "app_123456789"
      const role = "BASIC"

      await config.withApp(appId, () =>
        config.api.users.addWorkspaceIdToInvite("invalid_code", role, 400)
      )
    })

    it("should not allow builders to edit invites for apps they don't have access to", async () => {
      const { code } = await config.api.users.sendUserInvite(
        sendMailMock,
        structures.users.newEmail()
      )

      // Create a builder user with access to another app
      const builderUser = await config.createUser({
        ...structures.users.user(),
        builder: {
          global: false,
          apps: ["app_allowed_123"],
        },
        admin: { global: false },
      })

      await config.withUser(builderUser, () =>
        config.withApp("app_no_access", () =>
          config.api.users.addWorkspaceIdToInvite(code, "BASIC", 403)
        )
      )
    })

    it("should allow builders to edit invites for apps they have access to", async () => {
      const { code } = await config.api.users.sendUserInvite(
        sendMailMock,
        structures.users.newEmail()
      )
      const appId = "app_allowed_123"
      const role = "BASIC"

      // Create a builder user with access to the specific app
      const builderUser = await config.createUser({
        ...structures.users.user(),
        builder: {
          global: false,
          apps: [appId], // Has access to this specific app
        },
        admin: { global: false },
      })

      await config.login(builderUser)
      const res = await config.withUser(builderUser, () =>
        config.withApp(appId, () =>
          config.api.users.addWorkspaceIdToInvite(code, role, 200)
        )
      )
      expect(res.body.info.apps[appId]).toBe(role)
    })

    it("should not allow builders to edit invites for any app", async () => {
      const { code } = await config.api.users.sendUserInvite(
        sendMailMock,
        structures.users.newEmail()
      )
      const appId = "app_any_123"
      const role = "BASIC"

      // Create a global builder user
      const builderUser = await config.createUser({
        ...structures.users.user(),
        builder: {
          global: true,
        },
        admin: { global: false },
      })

      await config.login(builderUser)
      await config.withUser(builderUser, async () =>
        config.withApp(appId, () =>
          config.api.users.addWorkspaceIdToInvite(code, role, 403)
        )
      )
    })
  })

  describe("DELETE /api/global/users/invite/:code", () => {
    it("should be able to remove workspace id from invite", async () => {
      const email = structures.users.newEmail()
      const { code } = await config.api.users.sendUserInvite(
        sendMailMock,
        email
      )
      const appId = "app_123456789"
      const role = "BASIC"

      // First add the workspace
      await config.withApp(appId, () =>
        config.api.users.addWorkspaceIdToInvite(code, role)
      )

      // Then remove it
      const res = await config.withApp(appId, () =>
        config.api.users.removeWorkspaceIdFromInvite(code)
      )

      expect(res.body.info.apps).toBeDefined()
      expect(res.body.info.apps[appId]).toBeUndefined()
    })

    it("should handle removing non-existent workspace id", async () => {
      const email = structures.users.newEmail()
      const { code } = await config.api.users.sendUserInvite(
        sendMailMock,
        email
      )
      const appId = "app_nonexistent"

      const res = await config.withApp(appId, () =>
        config.api.users.removeWorkspaceIdFromInvite(code)
      )

      expect(res.body.info.apps).toBeDefined()
      expect(res.body.info.apps[appId]).toBeUndefined()
    })

    it("should handle invalid invite code", async () => {
      const appId = "app_123456789"

      await config.withApp(appId, () =>
        config.api.users.removeWorkspaceIdFromInvite("invalid_code", 400)
      )
    })

    it("should not allow builders to delete invites for apps they don't have access to", async () => {
      const { code } = await config.api.users.sendUserInvite(
        sendMailMock,
        structures.users.newEmail()
      )
      const appId = "app_no_access"
      const role = "BASIC"

      // First add the workspace as admin
      await config.withApp(appId, () =>
        config.api.users.addWorkspaceIdToInvite(code, role)
      )

      // Create a builder user with specific app access
      const builderUser = await config.createUser({
        ...structures.users.user(),
        builder: {
          global: false,
          apps: ["app_allowed_123"], // Different app than the one being tested
        },
        admin: { global: false },
      })

      await config.login(builderUser)
      await config.withUser(builderUser, () =>
        config.withApp(appId, () =>
          config.api.users.removeWorkspaceIdFromInvite(code, 403)
        )
      )
    })

    it("should allow builders to delete invites for apps they have access to", async () => {
      const { code } = await config.api.users.sendUserInvite(
        sendMailMock,
        structures.users.newEmail()
      )
      const appId = "app_allowed_456"
      const role = "BASIC"

      // First add the workspace as admin
      await config.withApp(appId, () =>
        config.api.users.addWorkspaceIdToInvite(code, role)
      )

      // Create a builder user with access to the specific app
      const builderUser = await config.createUser({
        ...structures.users.user(),
        builder: {
          global: false,
          apps: [appId], // Has access to this specific app
        },
        admin: { global: false },
      })

      await config.login(builderUser)
      const res = await config.withUser(builderUser, () =>
        config.withApp(appId, () =>
          config.api.users.removeWorkspaceIdFromInvite(code, 200)
        )
      )
      expect(res.body.info.apps[appId]).toBeUndefined()
    })
  })

  describe("POST /api/global/users/multi/invite", () => {
    it("should be able to generate an invitation", async () => {
      const newUserInvite = () => ({
        email: structures.users.newEmail(),
        userInfo: {},
      })
      const request = [newUserInvite(), newUserInvite()]

      const res = await config.api.users.sendMultiUserInvite(request)

      const body = res.body as InviteUsersResponse
      expect(body.successful.length).toBe(2)
      expect(body.unsuccessful.length).toBe(0)
      expect(sendMailMock).toHaveBeenCalledTimes(2)
      expect(events.user.invited).toHaveBeenCalledTimes(2)
    })

    it("should not be able to generate an invitation for existing user", async () => {
      const request = [{ email: config.user!.email, userInfo: {} }]

      const res = await config.api.users.sendMultiUserInvite(request)

      const body = res.body as InviteUsersResponse
      expect(body.successful.length).toBe(0)
      expect(body.unsuccessful.length).toBe(1)
      expect(body.unsuccessful[0].reason).toBe("Unavailable")
      expect(sendMailMock).toHaveBeenCalledTimes(0)
      expect(events.user.invited).toHaveBeenCalledTimes(0)
    })

    it("should not be able to generate an invitation for user that has already been invited", async () => {
      const email = structures.users.newEmail()
      await config.api.users.sendUserInvite(sendMailMock, email)

      jest.clearAllMocks()

      const request = [{ email: email, userInfo: {} }]
      const res = await config.api.users.sendMultiUserInvite(request)

      const body = res.body as InviteUsersResponse
      expect(body.successful.length).toBe(0)
      expect(body.unsuccessful.length).toBe(1)
      expect(body.unsuccessful[0].reason).toBe("Unavailable")
      expect(sendMailMock).toHaveBeenCalledTimes(0)
      expect(events.user.invited).toHaveBeenCalledTimes(0)
    })

    it("should not allow creator users to access multi-invite endpoint", async () => {
      const user = await createBuilderUser()

      const request = [
        {
          email: structures.users.newEmail(),
          userInfo: { admin: { global: true } },
        },
      ]

      const res = await config.withUser(user, () =>
        config.api.users.sendMultiUserInvite(request, 403)
      )
      expect(res.body.message).toBe("Admin user only endpoint.")
    })
  })

  describe("POST /api/global/users/bulk", () => {
    it("should ignore users existing in the same tenant", async () => {
      const user = await config.createUser()
      jest.clearAllMocks()

      const response = await config.api.users.bulkCreateUsers([user])

      expect(response.created?.successful.length).toBe(0)
      expect(response.created?.unsuccessful.length).toBe(1)
      expect(response.created?.unsuccessful[0].email).toBe(user.email)
      expect(events.user.created).toHaveBeenCalledTimes(0)
    })

    it("should ignore users existing in other tenants", async () => {
      const user = await config.createUser()
      jest.clearAllMocks()

      await tenancy.doInTenant(config.getTenantId(), async () => {
        const response = await config.api.users.bulkCreateUsers([user])

        expect(response.created?.successful.length).toBe(0)
        expect(response.created?.unsuccessful.length).toBe(1)
        expect(response.created?.unsuccessful[0].email).toBe(user.email)
        expect(events.user.created).toHaveBeenCalledTimes(0)
      })
    })

    it("should ignore accounts using the same email", async () => {
      const account = structures.accounts.account()
      const resp = await config.api.accounts.saveMetadata(account)
      const user = structures.users.user({ email: resp.email })
      jest.clearAllMocks()

      const response = await config.api.users.bulkCreateUsers([user])

      expect(response.created?.successful.length).toBe(0)
      expect(response.created?.unsuccessful.length).toBe(1)
      expect(response.created?.unsuccessful[0].email).toBe(user.email)
      expect(events.user.created).toHaveBeenCalledTimes(0)
    })

    it("should be able to bulk create users", async () => {
      const builder = structures.users.builderUser()
      const admin = structures.users.adminUser()
      const user = structures.users.user()

      const response = await config.api.users.bulkCreateUsers([
        builder,
        admin,
        user,
      ])

      expect(response.created?.successful.length).toBe(3)
      expect(response.created?.successful[0].email).toBe(builder.email)
      expect(response.created?.successful[1].email).toBe(admin.email)
      expect(response.created?.successful[2].email).toBe(user.email)
      expect(response.created?.unsuccessful.length).toBe(0)
      expect(events.user.created).toHaveBeenCalledTimes(3)
      expect(events.user.permissionAdminAssigned).toHaveBeenCalledTimes(1)
      expect(events.user.permissionBuilderAssigned).toHaveBeenCalledTimes(2)
    })
  })

  describe("POST /api/global/users", () => {
    it("should be able to create a basic user", async () => {
      const user = structures.users.user()

      await config.api.users.saveUser(user)

      expect(events.user.created).toHaveBeenCalledTimes(1)
      expect(events.user.updated).not.toHaveBeenCalled()
      expect(events.user.permissionBuilderAssigned).not.toHaveBeenCalled()
      expect(events.user.permissionAdminAssigned).not.toHaveBeenCalled()
    })

    it("should be able to create an admin user", async () => {
      const user = structures.users.adminUser()

      await config.api.users.saveUser(user)

      expect(events.user.created).toHaveBeenCalledTimes(1)
      expect(events.user.updated).not.toHaveBeenCalled()
      expect(events.user.permissionBuilderAssigned).toHaveBeenCalledTimes(1)
      expect(events.user.permissionAdminAssigned).toHaveBeenCalledTimes(1)
    })

    it("should be able to create a builder user", async () => {
      const user = structures.users.builderUser()

      await config.api.users.saveUser(user)

      expect(events.user.created).toHaveBeenCalledTimes(1)
      expect(events.user.updated).not.toHaveBeenCalled()
      expect(events.user.permissionBuilderAssigned).toHaveBeenCalledTimes(1)
      expect(events.user.permissionAdminAssigned).not.toHaveBeenCalled()
    })

    it("should be able to assign app roles", async () => {
      const user = structures.users.user()
      user.roles = {
        app_123: "role1",
        app_456: "role2",
      }

      await config.api.users.saveUser(user)

      const savedUser = await config.getUser(user.email)
      expect(events.user.created).toHaveBeenCalledTimes(1)
      expect(events.user.updated).not.toHaveBeenCalled()
      expect(events.role.assigned).toHaveBeenCalledTimes(2)
      expect(events.role.assigned).toHaveBeenCalledWith(savedUser, "role1")
      expect(events.role.assigned).toHaveBeenCalledWith(savedUser, "role2")
    })

    it("should not be able to create user that exists in same tenant", async () => {
      const user = await config.createUser()
      jest.clearAllMocks()
      delete user._id
      delete user._rev

      const response = await config.api.users.saveUser(user, 400)

      expect(response.body.message).toBe(
        `Email already in use: '${user.email}'`
      )
      expect(events.user.created).toHaveBeenCalledTimes(0)
    })

    it("should not be able to create user that exists in other tenant", async () => {
      const user = await config.createUser()
      jest.clearAllMocks()

      await tenancy.doInTenant(config.getTenantId(), async () => {
        delete user._id
        const response = await config.api.users.saveUser(user, 400)

        expect(response.body.message).toBe(
          `Email already in use: '${user.email}'`
        )
        expect(events.user.created).toHaveBeenCalledTimes(0)
      })
    })

    it("should not be able to create user with the same email as an account", async () => {
      const user = structures.users.user()
      const account = structures.accounts.cloudAccount()
      accounts.getAccount.mockReturnValueOnce(Promise.resolve(account))

      const response = await config.api.users.saveUser(user, 400)

      expect(response.body.message).toBe(
        `Email already in use: '${user.email}'`
      )
      expect(events.user.created).toHaveBeenCalledTimes(0)
    })

    it("should not be able to create a user with the same email and different casing", async () => {
      const user = structures.users.user()
      await config.api.users.saveUser(user)

      user.email = user.email.toUpperCase()
      await config.api.users.saveUser(user, 400)

      expect(events.user.created).toHaveBeenCalledTimes(1)
    })

    it("should not be able to bulk create a user with the same email and different casing", async () => {
      const user = structures.users.user()
      await config.api.users.saveUser(user)

      user.email = user.email.toUpperCase()
      await config.api.users.bulkCreateUsers([user])

      expect(events.user.created).toHaveBeenCalledTimes(1)
    })

    it("should not allow a non-admin user to create a new user", async () => {
      const nonAdmin = await config.createUser(structures.users.builderUser())
      await config.createSession(nonAdmin)

      const newUser = structures.users.user()
      await config.api.users.saveUser(
        newUser,
        403,
        config.authHeaders(nonAdmin)
      )
    })
  })

  describe("POST /api/global/users (update)", () => {
    it("should be able to update a basic user", async () => {
      const user = await config.createUser()
      jest.clearAllMocks()

      await config.api.users.saveUser(user)

      expect(events.user.created).not.toHaveBeenCalled()
      expect(events.user.updated).toHaveBeenCalledTimes(1)
      expect(events.user.permissionBuilderAssigned).not.toHaveBeenCalled()
      expect(events.user.permissionAdminAssigned).not.toHaveBeenCalled()
      expect(events.user.passwordForceReset).not.toHaveBeenCalled()
    })

    it("should not allow a user to update their own admin/builder status", async () => {
      const user = (await config.api.users.getUser(config.user!._id!))
        .body as User
      await config.api.users.saveUser({
        ...user,
        admin: {
          global: false,
        },
        builder: {
          global: false,
        },
      })
      const userOut = (await config.api.users.getUser(user._id!)).body
      expect(userOut.admin.global).toBe(true)
      expect(userOut.builder.global).toBe(true)
    })

    it("should be able to force reset password", async () => {
      const user = await config.createUser()
      jest.clearAllMocks()

      user.forceResetPassword = true
      user.password = "tempPassword"
      await config.api.users.saveUser(user)

      expect(events.user.created).not.toHaveBeenCalled()
      expect(events.user.updated).toHaveBeenCalledTimes(1)
      expect(events.user.permissionBuilderAssigned).not.toHaveBeenCalled()
      expect(events.user.permissionAdminAssigned).not.toHaveBeenCalled()
      expect(events.user.passwordForceReset).toHaveBeenCalledTimes(1)
    })

    it("should be able to update a basic user to an admin user", async () => {
      const user = await config.createUser()
      jest.clearAllMocks()

      await config.api.users.saveUser(structures.users.adminUser(user))

      expect(events.user.created).not.toHaveBeenCalled()
      expect(events.user.updated).toHaveBeenCalledTimes(1)
      expect(events.user.permissionBuilderAssigned).toHaveBeenCalledTimes(1)
      expect(events.user.permissionAdminAssigned).toHaveBeenCalledTimes(1)
    })

    it("should be able to update a basic user to a builder user", async () => {
      const user = await config.createUser()
      jest.clearAllMocks()

      await config.api.users.saveUser(structures.users.builderUser(user))

      expect(events.user.created).not.toHaveBeenCalled()
      expect(events.user.updated).toHaveBeenCalledTimes(1)
      expect(events.user.permissionBuilderAssigned).toHaveBeenCalledTimes(1)
      expect(events.user.permissionAdminAssigned).not.toHaveBeenCalled()
    })

    it("should be able to update an admin user to a basic user", async () => {
      const user = await config.createUser(structures.users.adminUser())
      jest.clearAllMocks()
      user.admin!.global = false
      user.builder!.global = false

      await config.api.users.saveUser(user)

      expect(events.user.created).not.toHaveBeenCalled()
      expect(events.user.updated).toHaveBeenCalledTimes(1)
      expect(events.user.permissionAdminRemoved).toHaveBeenCalledTimes(1)
      expect(events.user.permissionBuilderRemoved).toHaveBeenCalledTimes(1)
    })

    it("should be able to update an builder user to a basic user", async () => {
      const user = await config.createUser(structures.users.builderUser())
      jest.clearAllMocks()
      user.builder!.global = false

      await config.api.users.saveUser(user)

      expect(events.user.created).not.toHaveBeenCalled()
      expect(events.user.updated).toHaveBeenCalledTimes(1)
      expect(events.user.permissionBuilderRemoved).toHaveBeenCalledTimes(1)
      expect(events.user.permissionAdminRemoved).not.toHaveBeenCalled()
    })

    it("should be able to assign app roles", async () => {
      const user = await config.createUser()
      jest.clearAllMocks()
      user.roles = {
        app_123: "role1",
        app_456: "role2",
      }

      await config.api.users.saveUser(user)

      const savedUser = await config.getUser(user.email)
      expect(events.user.created).not.toHaveBeenCalled()
      expect(events.user.updated).toHaveBeenCalledTimes(1)
      expect(events.role.assigned).toHaveBeenCalledTimes(2)
      expect(events.role.assigned).toHaveBeenCalledWith(savedUser, "role1")
      expect(events.role.assigned).toHaveBeenCalledWith(savedUser, "role2")
    })

    it("should be able to unassign app roles", async () => {
      let user = structures.users.user()
      user.roles = {
        app_123: "role1",
        app_456: "role2",
      }
      user = await config.createUser(user)
      jest.clearAllMocks()
      user.roles = {}

      await config.api.users.saveUser(user)

      const savedUser = await config.getUser(user.email)
      expect(events.user.created).not.toHaveBeenCalled()
      expect(events.user.updated).toHaveBeenCalledTimes(1)
      expect(events.role.unassigned).toHaveBeenCalledTimes(2)
      expect(events.role.unassigned).toHaveBeenCalledWith(savedUser, "role1")
      expect(events.role.unassigned).toHaveBeenCalledWith(savedUser, "role2")
    })

    it("should be able to update existing app roles", async () => {
      let user = structures.users.user()
      user.roles = {
        app_123: "role1",
        app_456: "role2",
      }
      user = await config.createUser(user)
      jest.clearAllMocks()
      user.roles = {
        app_123: "role1",
        app_456: "role2-edit",
      }

      await config.api.users.saveUser(user)

      const savedUser = await config.getUser(user.email)
      expect(events.user.created).not.toHaveBeenCalled()
      expect(events.user.updated).toHaveBeenCalledTimes(1)
      expect(events.role.unassigned).toHaveBeenCalledTimes(1)
      expect(events.role.unassigned).toHaveBeenCalledWith(savedUser, "role2")
      expect(events.role.assigned).toHaveBeenCalledTimes(1)
      expect(events.role.assigned).toHaveBeenCalledWith(savedUser, "role2-edit")
    })

    it("should not be able to update email address", async () => {
      const email = structures.email()
      const user = await config.createUser(structures.users.user({ email }))
      user.email = "new@example.com"

      const response = await config.api.users.saveUser(user, 400)

      const dbUser = await config.getUser(email)
      user.email = email
      expect(user).toStrictEqual(dbUser)
      expect(response.body.message).toBe("Email address cannot be changed")
    })

    it("should not allow a builder users to update an existing user", async () => {
      const existingUser = await config.createUser(structures.users.user())
      const builderUser = await config.createUser(
        structures.users.builderUser()
      )
      await config.createSession(builderUser)

      await config.api.users.saveUser(
        existingUser,
        403,
        config.authHeaders(builderUser)
      )
    })

    describe("sso users", () => {
      function createSSOUser() {
        return config.doInTenant(() => {
          const user = structures.users.ssoUser()
          return userSdk.db.save(user, { requirePassword: false })
        })
      }

      function createPasswordUser() {
        return config.doInTenant(() => {
          const user = structures.users.user()
          return userSdk.db.save(user)
        })
      }

      it("should be able to update an sso user that has no password", async () => {
        const user = await createSSOUser()
        await config.api.users.saveUser(user)
      })

      it("sso support couldn't be used by admin. It is cloud restricted and needs internal key", async () => {
        const user = await config.createUser()
        const ssoId = "fake-ssoId"
        await config.api.users
          .addSsoSupportDefaultAuth(ssoId, user.email)
          .expect("Content-Type", /json/)
          .expect(403)
      })

      it("if user email doesn't exist, SSO support couldn't be added. Not found error returned", async () => {
        const ssoId = "fake-ssoId"
        const email = "fake-email@budibase.com"
        await config.api.users
          .addSsoSupportInternalAPIAuth(ssoId, email)
          .expect("Content-Type", /json/)
          .expect(404)
      })

      it("if user email exist, SSO support is added", async () => {
        const user = await createPasswordUser()
        const ssoId = "fakessoId"
        await config.api.users
          .addSsoSupportInternalAPIAuth(ssoId, user.email)
          .expect(200)
      })

      it("if user ssoId is already assigned, no change will be applied", async () => {
        const user = await createSSOUser()
        user.ssoId = "testssoId"
        await config.api.users
          .addSsoSupportInternalAPIAuth(user.ssoId, user.email)
          .expect(200)
      })
    })
  })

  describe("POST /api/global/users/bulk (delete)", () => {
    it("should not be able to bulk delete current user", async () => {
      const user = config.user!

      const response = await config.api.users.bulkDeleteUsers(
        [
          {
            userId: user._id!,
            email: "test@example.com",
          },
        ],
        400
      )

      expect(response.message).toBe("Unable to delete self.")
      expect(events.user.deleted).not.toHaveBeenCalled()
    })
  })

  describe("POST /api/global/users/search", () => {
    it("should be able to search by email", async () => {
      const user = await config.createUser()
      const response = await config.api.users.searchUsers({
        query: { string: { email: user.email } },
      })
      expect(response.body.data.length).toBe(1)
      expect(response.body.data[0].email).toBe(user.email)
    })

    it("should support fuzzy email fragments", async () => {
      const email = structures.users.newEmail()
      await config.createUser({ email })
      const fragment = email.slice(3, 12)
      const response = await config.api.users.searchUsers({
        query: { fuzzy: { email: fragment } },
      })
      expect(response.body.data.length).toBe(1)
      expect(response.body.data[0].email).toBe(email)
    })

    it("should be able to search by email with numeric prefixing", async () => {
      const user = await config.createUser()
      const response = await config.api.users.searchUsers({
        query: { string: { ["999:email"]: user.email } },
      })
      expect(response.body.data.length).toBe(1)
      expect(response.body.data[0].email).toBe(user.email)
    })

    it("should be able to search by _id", async () => {
      const user = await config.createUser()
      const response = await config.api.users.searchUsers({
        query: { equal: { _id: user._id } },
      })
      expect(response.body.data.length).toBe(1)
      expect(response.body.data[0]._id).toBe(user._id)
    })

    it("should be able to search by oneOf _id", async () => {
      const [user, user2, user3] = await Promise.all([
        config.createUser(),
        config.createUser(),
        config.createUser(),
      ])
      const response = await config.api.users.searchUsers({
        query: { oneOf: { _id: [user._id, user2._id] } },
      })
      expect(response.body.data.length).toBe(2)
      const foundUserIds = response.body.data.map((user: User) => user._id)
      expect(foundUserIds).toContain(user._id)
      expect(foundUserIds).toContain(user2._id)
      expect(
        response.body.data.find((user: User) => user._id === user3._id)
      ).toBeUndefined()
    })

    it("should be able to search by _id with numeric prefixing", async () => {
      const user = await config.createUser()
      const response = await config.api.users.searchUsers({
        query: { equal: { ["1:_id"]: user._id } },
      })
      expect(response.body.data.length).toBe(1)
      expect(response.body.data[0]._id).toBe(user._id)
    })

    it("should throw an error when using multiple filters on the same field", async () => {
      const user = await config.createUser()
      await config.api.users.searchUsers(
        {
          query: {
            string: {
              ["1:email"]: user.email,
              ["2:email"]: "something else",
            },
          },
        },
        { status: 400 }
      )
    })

    it("should throw an error when using multiple filters on the same field without prefixes", async () => {
      const user = await config.createUser()
      await config.api.users.searchUsers(
        {
          query: {
            string: {
              ["_id"]: user.email,
              ["999:_id"]: "something else",
            },
          },
        },
        { status: 400 }
      )
    })

    it("should throw an error when unimplemented options used", async () => {
      const user = await config.createUser()
      await config.api.users.searchUsers(
        {
          query: { equal: { firstName: user.firstName } },
        },
        { status: 400 }
      )
    })

    it("should throw an error if public query performed", async () => {
      await config.api.users.searchUsers({}, { status: 403, noHeaders: true })
    })

    it("should be able to search using logical conditions", async () => {
      const user = await config.createUser()
      const response = await config.api.users.searchUsers({
        query: {
          $and: {
            conditions: [
              {
                $and: {
                  conditions: [{ string: { email: user.email } }],
                },
              },
            ],
          },
        },
      })
      expect(response.body.data.length).toBe(1)
      expect(response.body.data[0].email).toBe(user.email)
    })

    it("should strip users if accessing as an end user", async () => {
      const user = await config.createUser({
        admin: { global: false },
        builder: { global: false },
      })
      const response = await config.api.users.searchUsers(
        {
          query: {},
        },
        { useHeaders: await config.login(user) }
      )
      for (let user of response.body.data) {
        expect(user.roles).toBeUndefined()
        expect(user.builder).toBeUndefined()
        expect(user.admin).toBeUndefined()
      }
    })
  })

  describe("POST /api/global/users/:userId/permission/:role", () => {
    it("should fail to assign CREATOR role when feature is not enabled", async () => {
      const user = await config.createUser()
      const workspaceId = "app_123456789"

      const res = await config.withApp(workspaceId, () =>
        config.api.users.addUserToWorkspace(
          user._id!,
          user._rev!,
          "CREATOR",
          400
        )
      )
      expect(res.body.message).toBe("Feature not enabled, please check license")
    })

    it("should assign CREATOR role and set builder properties", async () => {
      featureMocks.licenses.useAppBuilders()

      const user = await config.createUser()
      const workspaceId = "app_123456789"

      await config.withApp(workspaceId, () =>
        config.api.users.addUserToWorkspace(user._id!, user._rev!, "CREATOR")
      )

      const updatedUser = await config.getUser(user.email)
      expect(updatedUser.roles[workspaceId]).toBe("CREATOR")
      expect(updatedUser.builder?.creator).toBe(true)
      expect(updatedUser.builder?.apps).toEqual([workspaceId])
    })
    it("should keep builder creator flag when assigning non-CREATOR roles", async () => {
      const builderUser = await config.createUser({
        builder: {
          creator: true,
        },
      })
      const workspaceId = "app_creator_preserve"

      const res = await config.withApp(workspaceId, () =>
        config.api.users.addUserToWorkspace(
          builderUser._id!,
          builderUser._rev!,
          "BASIC"
        )
      )
      builderUser._rev = res.body._rev

      const updatedUser = await config.getUser(builderUser.email)
      expect(updatedUser.roles[workspaceId]).toBe("BASIC")
      expect(updatedUser.builder?.creator).toBe(true)
    })

    it("should keep builder creator flag when assigning CREATOR roles", async () => {
      featureMocks.licenses.useAppBuilders()

      const builderUser = await config.createUser({
        builder: {
          creator: true,
        },
      })
      const workspaceId = "app_creator_preserve"

      const res = await config.withApp(workspaceId, () =>
        config.api.users.addUserToWorkspace(
          builderUser._id!,
          builderUser._rev!,
          "CREATOR"
        )
      )
      builderUser._rev = res.body._rev

      const updatedUser = await config.getUser(builderUser.email)
      expect(updatedUser.roles[workspaceId]).toBe("CREATOR")
      expect(updatedUser.builder?.creator).toBe(true)
    })

    it("should maintain builder properties when user has multiple CREATOR roles", async () => {
      mocks.licenses.useAppBuilders()
      const user = await config.createUser()
      const workspaceId1 = "app_111111111"
      const workspaceId2 = "app_222222222"

      // Assign CREATOR role to two workspaces
      let res = await config.withApp(workspaceId1, () =>
        config.api.users.addUserToWorkspace(user._id!, user._rev!, "CREATOR")
      )
      user._rev = res.body._rev
      res = await config.withApp(workspaceId2, () =>
        config.api.users.addUserToWorkspace(user._id!, user._rev!, "CREATOR")
      )
      user._rev = res.body._rev

      let updatedUser = await config.getUser(user.email)
      expect(updatedUser.roles[workspaceId1]).toBe("CREATOR")
      expect(updatedUser.roles[workspaceId2]).toBe("CREATOR")
      expect(updatedUser.builder?.creator).toBe(true)
      expect(updatedUser.builder?.apps).toEqual(
        expect.arrayContaining([workspaceId1, workspaceId2])
      )

      // Remove from one workspace
      await config.withApp(workspaceId1, () =>
        config.api.users.removeUserFromWorkspace(user._id!, user._rev!)
      )

      updatedUser = await config.getUser(user.email)
      expect(updatedUser.roles[workspaceId1]).toBeUndefined()
      expect(updatedUser.roles[workspaceId2]).toBe("CREATOR")
      expect(updatedUser.builder?.creator).toBe(true)
      expect(updatedUser.builder?.apps).toEqual([workspaceId2])
    })

    it("should handle non-CREATOR role assignments without affecting builder properties", async () => {
      const user = await config.createUser()
      const workspaceId = "app_123456789"

      const res = await config.withApp(workspaceId, () =>
        config.api.users.addUserToWorkspace(user._id!, user._rev!, "BASIC")
      )

      expect(res.body._id).toBe(user._id)

      const updatedUser = await config.getUser(user.email)
      expect(updatedUser.roles[workspaceId]).toBe("BASIC")
      expect(updatedUser.builder?.creator).toBeUndefined()
      expect(updatedUser.builder?.apps).toBeUndefined()
    })

    it("should not allow non-admin users to modify workspace permissions", async () => {
      const regularUser = await config.createUser()
      const targetUser = await config.createUser()
      const workspaceId = "app_123456789"

      await config.login(regularUser)

      const res = await config.withUser(regularUser, () =>
        config.withApp(workspaceId, () =>
          config.api.users.addUserToWorkspace(
            targetUser._id!,
            targetUser._rev!,
            "CREATOR",
            403
          )
        )
      )
      expect(res.body.message).toBe(
        "Workspace Admin/Builder user only endpoint."
      )
    })
  })

  describe("DELETE /api/global/users/:userId", () => {
    it("should be able to destroy a basic user", async () => {
      const user = await config.createUser()
      jest.clearAllMocks()

      await config.api.users.deleteUser(user._id!)

      expect(events.user.deleted).toHaveBeenCalledTimes(1)
      expect(events.user.permissionBuilderRemoved).not.toHaveBeenCalled()
      expect(events.user.permissionAdminRemoved).not.toHaveBeenCalled()
    })

    it("should be able to destroy an admin user", async () => {
      const user = await config.createUser(structures.users.adminUser())
      jest.clearAllMocks()

      await config.api.users.deleteUser(user._id!)

      expect(events.user.deleted).toHaveBeenCalledTimes(1)
      expect(events.user.permissionBuilderRemoved).toHaveBeenCalledTimes(1)
      expect(events.user.permissionAdminRemoved).toHaveBeenCalledTimes(1)
    })

    it("should be able to destroy a builder user", async () => {
      const user = await config.createUser(structures.users.builderUser())
      jest.clearAllMocks()

      await config.api.users.deleteUser(user._id!)

      expect(events.user.deleted).toHaveBeenCalledTimes(1)
      expect(events.user.permissionBuilderRemoved).toHaveBeenCalledTimes(1)
      expect(events.user.permissionAdminRemoved).not.toHaveBeenCalled()
    })

    it("should not be able to destroy account owner", async () => {
      const user = await config.createUser()
      const account = structures.accounts.cloudAccount()
      accounts.getAccount.mockReturnValueOnce(Promise.resolve(account))

      const response = await config.api.users.deleteUser(user._id!, 400)

      expect(response.body.message).toBe("Account holder cannot be deleted")
    })

    it("should not be able to destroy account owner as account owner", async () => {
      const user = await config.user!
      const account = structures.accounts.cloudAccount()
      account.email = user.email
      accounts.getAccount.mockReturnValueOnce(Promise.resolve(account))

      const response = await config.api.users.deleteUser(user._id!, 400)

      expect(response.body.message).toBe("Unable to delete self.")
    })
  })

  describe("POST /api/global/users/onboard", () => {
    it("should successfully onboard a user", async () => {
      const response = await config.api.users.onboardUser([
        { email: structures.users.newEmail(), userInfo: {} },
      ])
      expect(response.successful.length).toBe(1)
      expect(response.unsuccessful.length).toBe(0)
    })

    it("should not onboard a user who has been invited", async () => {
      const email = structures.users.newEmail()
      await config.api.users.sendUserInvite(sendMailMock, email)

      const response = await config.api.users.onboardUser([
        { email, userInfo: {} },
      ])
      expect(response.successful.length).toBe(0)
      expect(response.unsuccessful.length).toBe(1)
    })
  })

  describe("PUT /api/global/users/tenant/owner", () => {
    it("should successfully change tenant owner email for existing user", async () => {
      const originalEmail = `original-${structures.uuid()}@example.com`
      const newEmail = `new-${structures.uuid()}@example.com`
      const tenantId = config.getTenantId()

      const user = await config.doInTenant(async () => {
        return await userSdk.db.save(
          {
            email: originalEmail,
            tenantId,
          } as any,
          { requirePassword: false, isAccountHolder: true }
        )
      })

      await config.api.users.changeTenantOwnerEmail(newEmail, originalEmail, [
        tenantId,
      ])

      const updatedUser = await config.doInTenant(async () => {
        return await userSdk.db.getUser(user._id!)
      })

      expect(updatedUser).toBeDefined()
      expect(updatedUser!.email).toBe(newEmail)
    })

    it("should handle multiple tenants", async () => {
      const originalEmail = `original-${structures.uuid()}@example.com`
      const newEmail = `new-${structures.uuid()}@example.com`
      const tenant1 = config.getTenantId()
      const tenant2 = structures.tenant.id()

      const user1 = await config.doInTenant(async () => {
        return await userSdk.db.save(
          {
            email: originalEmail,
            tenantId: tenant1,
          } as any,
          { requirePassword: false, isAccountHolder: true }
        )
      })

      const user2 = await config.doInSpecificTenant(tenant2, async () => {
        return await userSdk.db.save(
          {
            email: originalEmail,
            tenantId: tenant2,
          } as any,
          { requirePassword: false, isAccountHolder: true }
        )
      })

      await config.api.users.changeTenantOwnerEmail(newEmail, originalEmail, [
        tenant1,
        tenant2,
      ])

      const updatedUser1 = await config.doInTenant(async () => {
        return await userSdk.db.getUser(user1._id!)
      })

      const updatedUser2 = await config.doInSpecificTenant(
        tenant2,
        async () => {
          return await userSdk.db.getUser(user2._id!)
        }
      )

      expect(updatedUser1).toBeDefined()
      expect(updatedUser1!.email).toBe(newEmail)
      expect(updatedUser2).toBeDefined()
      expect(updatedUser2!.email).toBe(newEmail)
    })

    it("should not fail if user doesn't exist in tenant", async () => {
      const originalEmail = `nonexistent-${structures.uuid()}@example.com`
      const newEmail = `new-${structures.uuid()}@example.com`
      const tenantId = config.getTenantId()

      await config.api.users.changeTenantOwnerEmail(newEmail, originalEmail, [
        tenantId,
      ])

      const user = await config.doInTenant(async () => {
        return await userSdk.db.getUserByEmail(newEmail)
      })

      expect(user).toBeUndefined()
    })

    it("should handle empty tenant list", async () => {
      const originalEmail = `original-${structures.uuid()}@example.com`
      const newEmail = `new-${structures.uuid()}@example.com`

      await config.api.users.changeTenantOwnerEmail(newEmail, originalEmail, [])
    })

    it("should clear all OIDC-related fields", async () => {
      const originalEmail = `original-${structures.uuid()}@example.com`
      const newEmail = `new-${structures.uuid()}@example.com`
      const tenantId = config.getTenantId()
      const profile = {}
      const provider = "oidc"
      const providerType = "oidc"
      const thirdPartyProfile = {}
      const oauth2 = {}

      await config.doInTenant(async () => {
        await userSdk.db.save(
          {
            email: originalEmail,
            tenantId,
            profile,
            provider,
            providerType,
            thirdPartyProfile,
            oauth2,
          } as any,
          { requirePassword: false, isAccountHolder: true }
        )
      })

      await config.api.users.changeTenantOwnerEmail(newEmail, originalEmail, [
        tenantId,
      ])

      const updatedUser = (await config.doInTenant(async () => {
        return await userSdk.db.getUserByEmail(newEmail)
      })) as OIDCUser

      expect(updatedUser).toBeDefined()
      expect(updatedUser!.email).toBe(newEmail)
      expect(updatedUser.profile).toBe(undefined)
      expect(updatedUser.provider).toBe(undefined)
      expect(updatedUser.providerType).toBe(undefined)
      expect(updatedUser.thirdPartyProfile).toBe(undefined)
      expect(updatedUser.oauth2).toBe(undefined)
    })

    it("should require internal API headers", async () => {
      const originalEmail = `original-${structures.uuid()}@example.com`
      const newEmail = `new-${structures.uuid()}@example.com`
      const tenantId = config.getTenantId()

      await config.request
        .put(`/api/global/users/tenant/owner`)
        .send({
          newAccountEmail: newEmail,
          originalEmail,
          tenantIds: [tenantId],
        })
        .set(config.defaultHeaders())
        .expect(403)
    })
  })
})
